package com.nulldev.util.VariableAPI.util.random;

import static com.nulldev.util.VariableAPI.util.random.GlobalRandom.raw_nextLong;
import static com.nulldev.util.VariableAPI.util.random.RandomAccelMethod.FASTER_RNG_MRG32K3A;
import static com.nulldev.util.VariableAPI.util.random.RandomAccelMethod.FASTER_RNG_TAUS;
import static com.nulldev.util.VariableAPI.util.random.RandomAccelMethod.FASTER_RNG_TWISTER;
import static com.nulldev.util.VariableAPI.util.random.RandomAccelMethod.FASTER_RNG_WELL512;
import static com.nulldev.util.VariableAPI.util.random.RandomAccelMethod.UNIX_RANDOM;
import static com.nulldev.util.VariableAPI.util.random.RandomAccelMethod.UNIX_RANDOM_ALLOWED;
import static com.nulldev.util.VariableAPI.util.random.RandomAccelMethod.__get_accel_method;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Random;

import com.nulldev.util.JVM.Arguments;
import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.VariableAPI.util.random.fasterrng.FasterRandomAdapter;
import com.nulldev.util.VariableAPI.util.random.fasterrng.MRG32k3a;
import com.nulldev.util.VariableAPI.util.random.fasterrng.MersenneTwister;
import com.nulldev.util.VariableAPI.util.random.fasterrng.Taus88;
import com.nulldev.util.VariableAPI.util.random.fasterrng.Well512a;
import com.nulldev.util.data.Variables;
import com.nulldev.util.data.FastMath.FastRandom;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.manage.Macros;
import com.nulldev.util.security.hashing.MD5.MD5;

public class nRandom extends Random implements AutoCloseable {

	public final static boolean UNIX_RANDOM_LOCAL_FIO = Arguments.hasArgument("--nRandom.localFIO");
	public final static boolean UNIX_RANDOM_LOCAL_FIO_BUFFERING = Arguments.hasArgument("--nRandom.localFIOBuffering");

	private static final long serialVersionUID = -2032629862759525504L;
	private RandomAccelMethod accelMethod;
	private final byte[] seed;

	public nRandom() {
		this(__get_accel_method());
	}

	public nRandom(final boolean acceleration) {
		this.accelMethod = __get_accel_method(acceleration);
		if (this.accelMethod == RandomAccelMethod.FAST_RANDOM && this.__fr == null)
			this.__fr = new FastRandom();
		if ((this.accelMethod == FASTER_RNG_TAUS || this.accelMethod == FASTER_RNG_TWISTER || this.accelMethod == FASTER_RNG_WELL512
				|| this.accelMethod == FASTER_RNG_MRG32K3A) && this.__fr2 == null) {
			switch (this.accelMethod) {
				case FASTER_RNG_TAUS: {
					this.__fr2 = new FasterRandomAdapter(new Taus88());
					break;
				}
				case FASTER_RNG_TWISTER: {
					this.__fr2 = new FasterRandomAdapter(new MersenneTwister());
					break;
				}
				case FASTER_RNG_WELL512: {
					this.__fr2 = new FasterRandomAdapter(new Well512a());
					break;
				}
				case FASTER_RNG_MRG32K3A: {
					this.__fr2 = new FasterRandomAdapter(new MRG32k3a());
					break;
				}
				default:
					Macros.unreachable("Invalid accelMethod: " + this.accelMethod);
			}
			Variables.requireNonNull(this.__fr2, "__fr2");
		}
		this.seed = read(8);
		this.setSeed(MathUtil.getLong(seed, 0));
	}

	public nRandom(final RandomAccelMethod accelMethod) {
		this.accelMethod = Variables.requireNonNullObject(accelMethod, "accelMethod");
		if (this.accelMethod == RandomAccelMethod.FAST_RANDOM && this.__fr == null)
			this.__fr = new FastRandom();
		if ((this.accelMethod == FASTER_RNG_TAUS || this.accelMethod == FASTER_RNG_TWISTER || this.accelMethod == FASTER_RNG_WELL512
				|| this.accelMethod == FASTER_RNG_MRG32K3A) && this.__fr2 == null) {
			switch (this.accelMethod) {
				case FASTER_RNG_TAUS: {
					this.__fr2 = new FasterRandomAdapter(new Taus88());
					break;
				}
				case FASTER_RNG_TWISTER: {
					this.__fr2 = new FasterRandomAdapter(new MersenneTwister());
					break;
				}
				case FASTER_RNG_WELL512: {
					this.__fr2 = new FasterRandomAdapter(new Well512a());
					break;
				}
				case FASTER_RNG_MRG32K3A: {
					this.__fr2 = new FasterRandomAdapter(new MRG32k3a());
					break;
				}
				default:
					Macros.unreachable("Invalid accelMethod: " + this.accelMethod);
			}
			Variables.requireNonNull(this.__fr2, "__fr2");
		}
		this.seed = read(8);
		this.setSeed(MathUtil.getLong(seed, 0));
	}

	public nRandom(final byte[] seed) {
		this(seed, false);
	}

	public nRandom(final byte[] seed, final boolean acceleration) {
		this.accelMethod = __get_accel_method(acceleration);
		if (this.accelMethod == RandomAccelMethod.FAST_RANDOM && this.__fr == null)
			this.__fr = new FastRandom();
		if ((this.accelMethod == FASTER_RNG_TAUS || this.accelMethod == FASTER_RNG_TWISTER || this.accelMethod == FASTER_RNG_WELL512
				|| this.accelMethod == FASTER_RNG_MRG32K3A) && this.__fr2 == null) {
			switch (this.accelMethod) {
				case FASTER_RNG_TAUS: {
					this.__fr2 = new FasterRandomAdapter(new Taus88());
					break;
				}
				case FASTER_RNG_TWISTER: {
					this.__fr2 = new FasterRandomAdapter(new MersenneTwister());
					break;
				}
				case FASTER_RNG_WELL512: {
					this.__fr2 = new FasterRandomAdapter(new Well512a());
					break;
				}
				case FASTER_RNG_MRG32K3A: {
					this.__fr2 = new FasterRandomAdapter(new MRG32k3a());
					break;
				}
				default:
					Macros.unreachable("Invalid accelMethod: " + this.accelMethod);
			}
		}
		if (seed != null && seed.length >= 8) {
			this.seed = seed;
		} else {
			this.seed = read(8);
		}
		if (seed != null && seed.length >= 8)
			this.setSeed(MathUtil.getLong(seed, 0));
	}

	private FastRandom __fr = null;
	private FasterRandomAdapter __fr2 = null;
	private InputStream localFIO;

	public byte[] read(final int length) {
		return read(length, false);
	}

	private byte[] read(final int length, final boolean __INVOKED_LOCALLY) {
		if (length <= 0)
			return new byte[0];
		switch (Variables.requireNonNullObject(this.accelMethod, "accelMethod")) {
			case UNIX_RANDOM: {
				try {
					if (UNIX_RANDOM_LOCAL_FIO) {
						if (this.localFIO == null)
							this.localFIO = UNIX_RANDOM_LOCAL_FIO_BUFFERING
									? new BufferedInputStream(new FileInputStream(RandomAccelMethod.__UNIX_RANDOM_GET_FILE()), IOUtils.MEMORY_ALLOC_BUFFER)
									: new FileInputStream(RandomAccelMethod.__UNIX_RANDOM_GET_FILE());
						final byte[] data = new byte[length];
						IOUtils.readFully(this.localFIO, data, 0, length);
						return data;
					} else {
						final FileInputStream fis = new FileInputStream(RandomAccelMethod.__UNIX_RANDOM_GET_FILE());
						final byte[] data = new byte[length];
						IOUtils.readFully(fis, data, 0, length);
						fis.close();
						return data;
					}
				} catch (Exception e) {
					final byte[] b = new byte[length];
					__next_bytes(b);
					return b;
				}
			}
			case FAST_RANDOM:
				if (this.__fr != null)
					return this.__fr.bytes(length);
			case FASTER_RNG_TAUS:
			case FASTER_RNG_TWISTER:
			case FASTER_RNG_WELL512:
			case FASTER_RNG_MRG32K3A: {
				if (this.__fr2 != null)
					return this.__fr2.bytes(length);
			}
			default:
			case NO_ACCELERATION: {
				final byte[] b = new byte[length];
				__next_bytes(b);
				return b;
			}
		}
	}

	private void write(final byte[] bytes) {
		Variables.requireNonNull(bytes, "bytes");
		switch (Variables.requireNonNullObject(this.accelMethod, "accelMethod")) {
			case UNIX_RANDOM: {
				try {
					if (UNIX_RANDOM_LOCAL_FIO) {
						if (this.localFIO == null)
							this.localFIO = new FileInputStream(RandomAccelMethod.__UNIX_RANDOM_GET_FILE());
						IOUtils.readFully(this.localFIO, bytes, 0, bytes.length);
						break;
					} else {
						final FileInputStream fis = new FileInputStream(RandomAccelMethod.__UNIX_RANDOM_GET_FILE());
						IOUtils.readFully(fis, bytes, 0, bytes.length);
						fis.close();
						break;
					}
				} catch (Exception e) {
				}
			}
			case FAST_RANDOM: {
				this.__fr.nextBytes(bytes);
				break;
			}
			case FASTER_RNG_TAUS:
			case FASTER_RNG_TWISTER:
			case FASTER_RNG_WELL512:
			case FASTER_RNG_MRG32K3A: {
				this.__fr2.nextBytes(bytes);
				break;
			}
			case NO_ACCELERATION: {
				__next_bytes(bytes);
				break;
			}
			default:
				throw new UnsupportedOperationException("unreachable for " + this.accelMethod);
		}
	}

	private int __nextInt() {
		switch (Variables.requireNonNullObject(this.accelMethod, "accelMethod")) {
			case FAST_RANDOM:
				return this.__fr.nextInt();
			default:
				return super.next(32);
		}
	}

	private void __next_bytes(final byte[] bytes) {
		for (int i = 0, len = bytes.length; i < len;)
			for (int rnd = __nextInt(), n = Math.min(len - i, Integer.SIZE / Byte.SIZE); n-- > 0; rnd >>= Byte.SIZE)
				bytes[i++] = (byte) rnd;
	}

	@Override
	public void close() throws Exception {
		if (UNIX_RANDOM_LOCAL_FIO)
			IOUtils.closeQuietly(this.localFIO);
		if (this.__fr != null)
			IOUtils.closeQuietly(this.__fr);
	}

	@Override
	public synchronized double nextGaussian() {
		switch (this.accelMethod) {
			case FAST_RANDOM:
				if (this.__fr != null)
					return this.__fr.nextGaussian();
			case FASTER_RNG_TAUS:
			case FASTER_RNG_TWISTER:
			case FASTER_RNG_WELL512:
			case FASTER_RNG_MRG32K3A: {
				if (this.__fr2 != null)
					return this.__fr2.nextGaussian();
			}
			default:
				return super.nextGaussian();
		}
	}

	@Override
	public boolean nextBoolean() {
		switch (this.accelMethod) {
			case FAST_RANDOM:
				if (this.__fr != null)
					return this.__fr.nextBoolean();
			case FASTER_RNG_TAUS:
			case FASTER_RNG_TWISTER:
			case FASTER_RNG_WELL512:
			case FASTER_RNG_MRG32K3A: {
				if (this.__fr2 != null)
					return this.__fr2.nextBoolean();
			}
			default:
				return super.nextBoolean();
		}
	}

	@Override
	public double nextDouble() {
		switch (this.accelMethod) {
			case FAST_RANDOM:
				if (this.__fr != null)
					return this.__fr.nextDouble();
			case FASTER_RNG_TAUS:
			case FASTER_RNG_TWISTER:
			case FASTER_RNG_WELL512:
			case FASTER_RNG_MRG32K3A: {
				if (this.__fr2 != null)
					return this.__fr2.nextDouble();
			}
			default:
				return (((long) (next(26)) << 27) + next(27)) * DOUBLE_UNIT;
		}
	}

	private static final double DOUBLE_UNIT = 0x1.0p-53; // 1.0 / (1L << 53)

	@Override
	public void nextBytes(final byte[] bytes) {
		switch (this.accelMethod) {
			case FAST_RANDOM: {
				if (this.__fr != null) {
					this.__fr.nextBytes(bytes);
					break;
				}
			}
			case FASTER_RNG_TAUS:
			case FASTER_RNG_TWISTER:
			case FASTER_RNG_WELL512:
			case FASTER_RNG_MRG32K3A: {
				if (this.__fr2 != null) {
					this.__fr2.nextBytes(bytes);
					break;
				}
			}
			default:
				write(bytes);
				break;
		}
	}

	@Override
	public float nextFloat() {
		switch (this.accelMethod) {
			case FAST_RANDOM:
				if (this.__fr != null)
					return this.__fr.nextFloat();
			case FASTER_RNG_TAUS:
			case FASTER_RNG_TWISTER:
			case FASTER_RNG_WELL512:
			case FASTER_RNG_MRG32K3A: {
				if (this.__fr2 != null)
					return this.__fr2.nextFloat();
			}
			default:
				return super.nextFloat();
		}
	}

	@Override
	public int nextInt() {
		switch (this.accelMethod) {
			case FAST_RANDOM:
				if (this.__fr != null)
					return this.__fr.nextInt();
			case FASTER_RNG_TAUS:
			case FASTER_RNG_TWISTER:
			case FASTER_RNG_WELL512:
			case FASTER_RNG_MRG32K3A: {
				if (this.__fr2 != null)
					return this.__fr2.nextInt();
			}
			default:
				return next(32);
		}
	}

	protected long nextL(final int bits) {
		return (MathUtil.getLong(read(8, true), 0) >>> (64 - bits));
	}

	@Override
	protected int next(final int bits) {
		return (int) (MathUtil.getLong(read(8, true), 0) >>> (64 - bits));
	}

	public long makeLong() {
		return nextL(32);
	}

	public int makeInt() {
		return next(32);
	}

	public int makeInt(final int min, final int max) {
		return this.nextInt((max - min) + 1) + min;
	}

	public boolean makeBoolean() {
		return next(1) < next(1);
	}

	public char makeChar() {
		return (char) (next(8) >> 48);
	}

	public char makeCharStrict() {
		return (char) (makeInt(48, 122));
	}

	public char makeCharASCII() {
		return (char) (makeBoolean() ? makeInt(97, 122) : makeInt(65, 90));
	}

	public String makeString(final int length) {
		String s = "";
		for (int i = 0; i < length; i++) {
			s += makeChar();
		}
		return s;
	}

	public String makeStringStrict(final int length) {
		String s = "";
		for (int i = 0; i < length; i++) {
			s += makeCharStrict();
		}
		return s;
	}

	public String makeStringASCII(final int length) {
		String s = "";
		for (int i = 0; i < length; i++) {
			s += makeCharASCII();
		}
		return s;
	}

	public byte[] getSeed() {
		return this.seed;
	}

	public String getSeedMD5() {
		return MD5.getMD5Hex(this.seed);
	}

	public void allocate(final int required) {
		if (this.accelMethod == RandomAccelMethod.FAST_RANDOM && this.__fr != null) {
			this.__fr.awaitBuffer(required);
		}
	}

	public byte[] bytes(final int length) {
		return this.read(length);
	}

	@Override
	public int nextInt(final int bound) {
		switch (this.accelMethod) {
			case FAST_RANDOM:
				if (this.__fr != null)
					return this.__fr.nextInt(bound);
			case FASTER_RNG_TAUS:
			case FASTER_RNG_TWISTER:
			case FASTER_RNG_WELL512:
			case FASTER_RNG_MRG32K3A: {
				if (this.__fr2 != null)
					return this.__fr2.nextInt(bound);
			}
			case UNIX_RANDOM:
			case NO_ACCELERATION:
			default: {
				if (bound <= 0)
					throw new IllegalArgumentException();

				int r = next(31);
				int m = bound - 1;
				if ((bound & m) == 0) {
					r = (int) ((bound * (long) r) >> 31);
				} else {
					for (int u = r; u - (r = u % bound) + m < 0; u = next(31))
						;
				}
				return r;
			}
		}
	}

	@Override
	public long nextLong() {
		switch (this.accelMethod) {
			case FAST_RANDOM:
				if (this.__fr != null)
					return this.__fr.nextLong();
			case FASTER_RNG_TAUS:
			case FASTER_RNG_TWISTER:
			case FASTER_RNG_WELL512:
			case FASTER_RNG_MRG32K3A: {
				if (this.__fr2 != null)
					return this.__fr2.nextLong();
			}
			default:
				return this.makeLong();
		}
	}

	@Override
	public synchronized void setSeed(long seed) {
		if (seed == 0)
			return;
		if (this.__fr != null)
			this.__fr.setSeed(seed);
		if (this.__fr2 != null)
			this.__fr2.setSeed(seed);
		super.setSeed(seed);
	}

	public long randLong(final long min, final long max) {
		switch (this.accelMethod) {
			case FAST_RANDOM:
				if (this.__fr != null)
					return raw_nextLong(this.__fr, min, max);
			case FASTER_RNG_TAUS:
			case FASTER_RNG_TWISTER:
			case FASTER_RNG_WELL512:
			case FASTER_RNG_MRG32K3A: {
				if (this.__fr2 != null)
					return raw_nextLong(this.__fr2, min, max);
			}
			default:
				return raw_nextLong(this, min, max);
		}
	}

	public void setFastRandom(final boolean fastRNG) {
		if (UNIX_RANDOM_ALLOWED && this.accelMethod == UNIX_RANDOM)
			return;
		this.accelMethod = __get_accel_method(fastRNG);
		if (this.accelMethod == RandomAccelMethod.FAST_RANDOM && this.__fr == null)
			this.__fr = new FastRandom();
		if ((this.accelMethod == FASTER_RNG_TAUS || this.accelMethod == FASTER_RNG_TWISTER || this.accelMethod == FASTER_RNG_WELL512
				|| this.accelMethod == FASTER_RNG_MRG32K3A) && this.__fr2 == null) {
			switch (this.accelMethod) {
				case FASTER_RNG_TAUS: {
					this.__fr2 = new FasterRandomAdapter(new Taus88());
					break;
				}
				case FASTER_RNG_TWISTER: {
					this.__fr2 = new FasterRandomAdapter(new MersenneTwister());
					break;
				}
				case FASTER_RNG_WELL512: {
					this.__fr2 = new FasterRandomAdapter(new Well512a());
					break;
				}
				case FASTER_RNG_MRG32K3A: {
					this.__fr2 = new FasterRandomAdapter(new MRG32k3a());
					break;
				}
				default:
					Macros.unreachable("Invalid accelMethod: " + this.accelMethod);
			}
			Variables.requireNonNull(this.__fr2, "__fr2");
		}
	}

	public void setAccelMethod(final RandomAccelMethod accelMethod) {
		if (UNIX_RANDOM_ALLOWED && this.accelMethod == UNIX_RANDOM)
			return;
		this.accelMethod = Variables.requireNonNullObject(accelMethod, "accelMethod");
		if (this.accelMethod == RandomAccelMethod.FAST_RANDOM && this.__fr == null)
			this.__fr = new FastRandom();
		if ((this.accelMethod == FASTER_RNG_TAUS || this.accelMethod == FASTER_RNG_TWISTER || this.accelMethod == FASTER_RNG_WELL512
				|| this.accelMethod == FASTER_RNG_MRG32K3A) && this.__fr2 == null) {
			switch (this.accelMethod) {
				case FASTER_RNG_TAUS: {
					this.__fr2 = new FasterRandomAdapter(new Taus88());
					break;
				}
				case FASTER_RNG_TWISTER: {
					this.__fr2 = new FasterRandomAdapter(new MersenneTwister());
					break;
				}
				case FASTER_RNG_WELL512: {
					this.__fr2 = new FasterRandomAdapter(new Well512a());
					break;
				}
				case FASTER_RNG_MRG32K3A: {
					this.__fr2 = new FasterRandomAdapter(new MRG32k3a());
					break;
				}
				default:
					Macros.unreachable("Invalid accelMethod: " + this.accelMethod);
			}
			Variables.requireNonNull(this.__fr2, "__fr2");
		}
	}
}